﻿/*
Copyright (c) 2010 Andrew Mattie

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
 */

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.IO;

namespace cc2vs
{
	class Program
	{
		private static SortedDictionary<String, List<String>> _argsLookup;
		private static String _programDir = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);

		static int Main(string[] args)
		{
			_argsLookup = ParseCommandArgs(args);

			if (!_argsLookup.ContainsKey("input-dir"))
			{
				Console.Error.WriteLine("The 'input-dir' argument hasn't been provided. Try adding at least one --input-dir \"C:\\PATH\\TO THE\\DIR\" argument to this command");
				return -1;
			}
			if (_argsLookup.ContainsKey("use-closure-dependencies") || _argsLookup.ContainsKey("path-to-closure") && !_argsLookup.ContainsKey("namespace"))
			{
				Console.Error.WriteLine("The 'namespace' argument must be provided if 'use-closure-dependencies' or 'path-to-closure' is set." + 
					" 'namespace' is your app's main dot-delimited path declared with goog.provide (e.g. mycompany.myapp).");
				return -1;
			}

			try
			{
				ValidateJsAssets();


				// always want to exit clean since we're really just looking to be a proxy for the messages from the closure compiler
				// ... although this doesn't seem to be working. i suspect once the closure compiler writes to stderr, there's no coming back
				return 0;
			}
			catch (Exception ex)
			{
				Console.Error.WriteLine(ex.Message);
				return -1;
			}
		}
		private static SortedDictionary<String, List<String>> ParseCommandArgs(String[] args)
		{
			SortedDictionary<String, List<String>> parsedArgs = new SortedDictionary<String, List<String>>();
			String lastNamedArg = null;

			foreach (String arg in args)
			{
				if (arg.StartsWith("-"))
				{
					if (arg.Contains("="))
					{
						String[] splitArg = arg.Split('=');
						String key = splitArg[0].TrimStart('-');
						String value = splitArg[1].Trim('"');
						List<String> existingArgs;

						if (parsedArgs.TryGetValue(key, out existingArgs))
							existingArgs.Add(value);
						else
							parsedArgs[key] = new List<string>() { value };
					}
					else if (String.IsNullOrEmpty(lastNamedArg))
					{
						lastNamedArg = arg;
					}
					else
					{
						parsedArgs[arg.TrimStart('-')] = null;
						lastNamedArg = null;
					}
					continue;
				}
				else
				{
					String key, value;
					List<String> existingArgs;

					if (String.IsNullOrEmpty(lastNamedArg))
						throw new Exception("The command line arguments were invalid. An arg value was encountered without an immediately preceeding argument name.");

					key = lastNamedArg.TrimStart('-');
					value = arg.Trim('"');

					if (parsedArgs.TryGetValue(key, out existingArgs))
						existingArgs.Add(value);
					else
						parsedArgs[key] = new List<String>() { value };
					lastNamedArg = null;
				}
			}

			return parsedArgs;
		}

		private static void ValidateJsAssets()
		{
			ProcessStartInfo validatorArgs = new ProcessStartInfo("java.exe");
			StringBuilder validatorCommandArgs = new StringBuilder(1000);
			String outputPath;
			List<String> pathToClosure, outputPathArg;
			String errorMessages;
			String ccArgIndicator = "ccarg-";

			foreach (String jsFile in GatherJsAssets())
				validatorCommandArgs.AppendFormat("--js \"{0}\" ", jsFile);

			if (_argsLookup.TryGetValue("path-to-closure", out pathToClosure))
				validatorCommandArgs.AppendFormat("--js \"{0}\\goog\\deps.js\" ", pathToClosure[0]);

			foreach (var argList in _argsLookup.Where(arg => arg.Key.StartsWith(ccArgIndicator)))
				foreach (String arg in argList.Value)
					validatorCommandArgs.AppendFormat("--{0} {1} ", argList.Key.Substring(ccArgIndicator.Length), arg.Contains(' ') ? "\"" + arg + "\"" : arg);

			if (!_argsLookup.TryGetValue("compiled-js-ouput-path", out outputPathArg))
				outputPath = String.Format("{0}\\compiled.js", System.Environment.GetEnvironmentVariable("TEMP"));
			else
				outputPath = outputPathArg[0];

			validatorArgs.Arguments = String.Format("-jar \"{0}\\lib\\closure-compiler.jar\" {1} --js_output_file \"{2}\"", _programDir, validatorCommandArgs.ToString(), outputPath);
			validatorArgs.UseShellExecute = false;
			validatorArgs.RedirectStandardError = true;

			using (Process validator = new Process())
			{
				validator.StartInfo = validatorArgs;
				validator.Start();
				errorMessages = validator.StandardError.ReadToEnd();
				validator.WaitForExit();
				validator.Dispose();
			}

			WriteJsAssetsErrors(errorMessages);
		}
		private static List<String> GatherJsAssets()
		{
			List<String> pathToClosure, excludePaths;
			List<String> jsFiles = new List<String>(100);

			if (_argsLookup.TryGetValue("path-to-closure", out pathToClosure) || _argsLookup.ContainsKey("use-closure-dependencies"))
			{
				FileInfo closureBuilderScript = new FileInfo(pathToClosure[0] + @"\bin\build\closurebuilder.py");
				String closureBuilderInputs, closureBuilderNamespaceArg, closureBuilderOutput, closureBuilderError;
				ProcessStartInfo closureBuilderArgs = new ProcessStartInfo("python.exe");

				closureBuilderInputs = _argsLookup["input-dir"].Aggregate("", (agg, input) => String.Format("{0} --root \"{1}\"", agg, input));
				if (pathToClosure != null)
				{
					// add trailing .. to closure path that third_party dir can be brought in. this was done for legacy reasons.
					closureBuilderInputs += String.Format(" --root \"{0}\\..\"", pathToClosure[0]);
				}
				closureBuilderNamespaceArg = String.Format(" --namespace \"{0}\"", _argsLookup["namespace"][0]);

				closureBuilderArgs.Arguments = String.Format("\"{0}\" ", closureBuilderScript.FullName) + closureBuilderInputs + closureBuilderNamespaceArg;
				closureBuilderArgs.UseShellExecute = false;
				closureBuilderArgs.RedirectStandardOutput = true;
				closureBuilderArgs.RedirectStandardError = true;

				if (!closureBuilderScript.Exists)
					throw new Exception(String.Format("closurebuilder.py couldn't be found in '{0}'", closureBuilderScript.DirectoryName));

				using (Process closureBuilder = new Process())
				{
					closureBuilder.StartInfo = closureBuilderArgs;
					closureBuilder.Start();
					closureBuilderOutput = closureBuilder.StandardOutput.ReadToEnd();
					closureBuilderError = closureBuilder.StandardError.ReadToEnd();
					closureBuilder.WaitForExit();
				}

				if (String.IsNullOrEmpty(closureBuilderOutput))
				{
					Console.Error.Write(closureBuilderError);
					throw new Exception("Aborting cc2vs due to errors in dependency calculation. See console errors.");
				}

				Console.WriteLine("Dependency calculations follow");
				foreach (String line in closureBuilderOutput.Split('\n'))
				{
					Console.WriteLine(line);

					String trimmedLine = line.Trim();
					if (!String.IsNullOrEmpty(trimmedLine))
						jsFiles.Add(trimmedLine);
				}
				Console.WriteLine("End dependency calculations");
			}
			else
			{
				foreach (String dir in _argsLookup["input-dir"])
					foreach (String jsAsset in Directory.GetFiles(dir, "*.js", SearchOption.AllDirectories))
						jsFiles.Add(jsAsset);
			}

			if (_argsLookup.TryGetValue("exclude", out excludePaths))
			{
				foreach (String path in excludePaths)
					jsFiles = jsFiles.Where(f => !f.StartsWith(path)).ToList();
			}
			return jsFiles.Distinct().ToList();
		}
		private static void WriteJsAssetsErrors(String errorMessages)
		{
			Regex errorMatcher = new Regex(@"^(?<fullPath>.+):(?<lineNumber>\d+): (?<level>WARNING|ERROR) \- (?<message>.+)");
			String[] errors = errorMessages.Split('\n');

			for (int i = 0; i < errors.Length; i += 4)
			{
				Match errorMatch = errorMatcher.Match(errors[i]);
				int errorStartColumn;

				if (!errorMatch.Success)
					continue;

				errorStartColumn = errors[i + 2].LastIndexOfAny(new char[] { ' ', '\t' }) + 2;

				Console.WriteLine("{0}({1},{2}): {3} JS0000: {4}",
					errorMatch.Groups["fullPath"].Value,
					errorMatch.Groups["lineNumber"].Value,
					errorStartColumn,
					errorMatch.Groups["level"].Value.ToLower(),
					errorMatch.Groups["message"].Value
				);
			}
		}
	}
}
